// Copyright (c) 2009 - Quildreen <quildreen@gmail.com>                     
//                                                                          
//  Permission is hereby granted, free of charge, to any person              
//  obtaining a copy of this software and associated documentation files     
//  (the "Software"), to deal in the Software without restriction,           
//  including without limitation the rights to use, copy, modify, merge,     
//  publish, distribute, sublicense, and/or sell copies of the Software,     
//  and to permit persons to whom the Software is furnished to do so,        
//  subject to the following conditions:                                     
//                                                                           
//  The above copyright notice and this permission notice shall be           
//  included in all copies or substantial portions of the Software.          
//                                                                           
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,          
//  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF       
//  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                    
//  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE   
//  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION   
//  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION    
//  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.          


/**
 * lily
 *
 * Lily is a modular framework for game development in JavaScript, using the
 * new features of HTML5, CSS 3 and JavaScript APIs. It's designed to be easily
 * extensible and simplify the creation of webgames in JavaScript.
 *
 * The [[lily]] module is the entry-point to the lily framework, providing
 * functions to load the rest of the lily package and define namespaces. 
 **/
var lily = {
    /** 
     * lily._all -> Array
     * 
     * List of all default modules to be imported in lily package.
     **/
    _all: [],


    /**
     * lily._imported -> Object
     * 
     * Tracks all modules we've already successfully imported, so we won't
     * import the same module twice.
     **/
    _imported: {
        script: {},
        css: {}
    },


    /**
     * lily._last_imported -> Array
     * 
     * List containing the very last modules imported by [[lily.require]].
     **/
    _last_imported: [],


    /**
     * lily._import_callbacks -> Object
     * 
     * Holds all the callbacks used for module loading. The callbacks are
     * mapped as `module = [callback array]`.
     **/
    _import_callbacks: {},


    /**
     * lily._temp -> Object
     * 
     * Holds all the temporary variables defined in lily.
     **/
    _temp: {},


    /**
     * lily.version -> String (read only)
     * 
     * Current version of the lily package. 
     **/
    version: '0.0.1a',


    /**
     * lily.prototype_version -> String (read only)
     *
     * The version of the Prototype library that's required to run the lily
     * framework.
     **/
    prototype_version: '1.6.1',


    /**
     * lily.path -> String
     * 
     * The path to the lily modules. This will be used as the default path when
     * modules are required without giving an explicit path.
     **/
    path: '',


    /**
     * lily.module_version -> String
     * 
     * The default module version to be imported.
     * 
     * This string will be added to the module URL, just before the file
     * extension. So if you require the module `time` and the `module_version`
     * is set to `-min`, the framework will load the url `time-min.js`.
     **/
    module_version: '',


    /**
     * lily.import_method -> String
     * 
     * The default kind of method to use when importing a module.
     * 
     * By default, `ajax` and `xdomain` methods are defined, but this can be
     * any function name defined in the [[lily.importer]] namespace.
     **/
    import_method: 'ajax',


    /**
     * lily.object_count -> Number (read only)
     * 
     * The number of objects created within the lily package.
     * 
     * We use this number to generate unique IDs to all objects in the
     * framework, since the Object reference can't be used as a property key.
     **/
    object_count: 0,


    /**
     * lily.logger
     * 
     * A namespace that takes care of error handling and application logs.
     *
     * It actually implements a dummy logger, so the logger module can
     * replace it with additional features.
     **/
    logger: {
        error   : function(){},
        log     : function(){},
        logMore : function(){}
    },


    /**
     * lily.importer
     *
     * A namespace for all importing methods used by the lily framework.
     **/
    importer: {
        /**
         * lily.importer.ajax(url[, options]) -> undefined
         * - url     (String): the URL of the resource to load
         * - options (Object): optional object to pass as additional options
         *                     for the `Ajax.Request` object. 
         * 
         * Imports a javascript module using Ajax.
         * 
         * This method can only be used by requests originated from the same
         * domain that your page is running on, since browsers implement a
         * same-origin policy for security reasons.
         * 
         * Another problem with this method is that information on file and
         * line numbers are lost, making it harder to debug.
         * 
         * Later this method can be used to provide additional features to
         * module importing, since it allows for manipulations on the imported
         * code before interpreting it.
         *
         * __see also__:
         * 
         * - [Prototype Ajax.Request reference][1]
         * 
         * [1]: http://api.prototypejs.org/ajax/ajax/request/
         **/
        ajax: function(url, options)
        {
            new Ajax.Request(url, options);
        },


        /**
         * lily.importer.xdomain(url) -> undefined
         * - url (String): the URL of the resource to load.
         * 
         * Imports the required module using linked script insertions on the page.
         * 
         * Unlike the Ajax requesting method, this also allows cross-domain
         * requests, meaning your scripts may reside outside of the domain your
         * page is running at.
         **/
        xdomain: function(url)
        {
            var s = new Element("script");
            s.src = url;
            s.type = "text/javascript";
            $$("head")[0].insert(s);
        }
    },


    /**
     * lily._hasDependencies() -> Boolean
     * 
     * Verifies if the dependencies are properly loaded.
     * 
     * Currently it checks if the Prototype library is available and if the
     * version available is equal or greater than the framework's required
     * version of the Prototype library.
     **/
    _hasDependencies: function()
    {
        // Converts a version string to its numeric representation. 
        function parseVersion(version)
        {
            var v = version.split(".");
            return parseInt(v[0]) * 100000 + parseInt(v[1]) * 1000
                 + parseInt(v[2]);
        }

        if (!Prototype || parseVersion(Prototype.Version)
            < parseVersion(lily.prototype_version)) {

            throw new Error("lily requires the Prototype JavaScript"
                          + "framework >= " + lily.prototype_version);
        } else {
            return true;
        }
    },


    /**
     * lily._import(module[, options]) -> undefined
     * - module  (String): the name of the module to import
     * - options (Object): Additional options that can be passed to override
     *                     the default framework importing options.
     * 
     * Imports a single JavaScript module, if we haven't loaded it yet.
     * 
     * The module name follows Java's packaging conventions, where submodules
     * are separated by a dot. So if you require `my.game.module`, lily will
     * look for `my/game/module.js`.
     * 
     * There are three general options accepted by the framework loader:
     * 
     * - path: the root URL where we'll look for the modules.
     * - version: the version of the module to import.
     * - method: the method to use when importing the method.
     * 
     * Some importing methods (like [[lily.importer.ajax]]) may accept
     * additional options, so look at the correspondent importing method in
     * [[lily.importer]] namespace for them.
     **/
    _import: function(module)
    {
        var options = Object.extend(
            {
                path: lily.path,
                version: lily.module_version,
                method: lily.import_method,
                asynchronous: false,
                evalJS: 'force'
            }, arguments[1] || {});

        if (options.path && !options.path.endsWith("/")) {
            options.path += "/";
        }

        if (module.endsWith(".*")) {
            module = module.substr(0, module.length-1) + "__all__";
        }

        if (!lily._imported.script[module]) {
            lily._imported.script[module] = "loading";

            var url = options.path + module.gsub(".", "/") + options.version
                    + ".js";

            lily.importer[options.method](url, options);
        }
    },


    /**
     * lily._importCSS(module[, options]) -> undefined
     * - module  (String): the name of the CSS module to import.
     * - options (Object): additional options to pass to the importer.
     * 
     * Imports a single CSS file, if we haven't loaded it yet.
     * 
     * The CSS importing method uses only the [[lily.importer.xdomain]] method,
     * thus the options that may be passed to it are restricted to `path` and
     * `version`.
     *  
     * Module naming rule follows the same lines defined in [[lily._import]],
     * where dots are replaced by slashes.
     **/
    _importCSS: function(module)
    {
        var options = Object.extend(
            {
                path: lily.path,
                version: lily.module_version
            }, arguments[1] || {});

        if (options.path && !options.path.endsWith("/")) {
            options.path += "/";
        }

        if (!lily._imported.css[module]) {
            lily._imported.css[module] = true;

            var elm = new Element("link", {"href": options.path
                                + module.gsub(".", "/") + options.version
                                + ".css", "id": module,
                                "media": options.media, rel: options.rel});

            elm.type = "text/css";
            $$("head")[0].insert(elm);
        }
    },


    /**
     * lily._load() -> undefined
     * 
     * Loads and initializes the lily framework.
     **/
    _load: function()
    {
        if (!lily._hasDependencies()){
            return;
        }

        var re = /lily\.js.*/;

        // get the imported lily package
        var src = $$("script[src]").findAll(
            function(s)
            {
                return (s.src.match(re));
            }
        );

        if (src.length > 1) {
            throw new Error("lily framework imported more than once.");
        }
        src = src[0].src;

        // verify the optional parameters passed on the import URL
        var params = src.toQueryParams();

        var modules = (params.modules && params.modules.split(",")) || [];

        lily.path = src.replace(re, '');
        lily.module_version = params.version || "";
        lily.import_method = params.import_method || "ajax";

        if (modules.indexOf(["all"]) >= 0) {
            modules = modules.concat(lily._all).without("all");
        }

        for (var i=0, len=modules.length; i < len; ++i) {
            lily._import(modules[i]);
        }
    },


    /**
     * lily.require(modules...[, options]) -> undefined
     * - modules  (String): modules to import, as positional arguments.
     * - options  (Object): additional options to pass to the importer.
     * 
     * Requires a javascript module. If it wasn't loaded yet, we'll load it.
     *
     * You can pass a range of modules on the arguments function, they'll be
     * imported in the declarated order. If the last object is a Hash or
     * Object we'll use it as the import options.
     *
     * For a list of options that you can pass see [[lily._import]].
     * 
     * The module name follows Java's packaging conventions, where submodules
     * are separated by a dot. So if you require `my.game.module`, lily will
     * look for `my/game/module.js`.
     **/
    require: function()
    {
        var modules = $A(arguments);

        var arg = modules.pop();
        if (Object.isString(arg) || Object.isArray(arg) ||
            Object.isNumber(arg) || Object.isFunction(arg)) {
            modules.push(arg);
            arg = {};
        }

        lily._last_imported = modules;

        for (var i=0, len=modules.length; i < len; ++i) {
            lily._import(modules[i], arg);
        }
    },


    /**
     * lily.requireCSS(modules...[, options]) -> undefined
     * 
     * Requires a CSS module. If it wasn't loaded yet, we'll load it.
     *
     * You can pass a range of modules on the arguments function, they'll be
     * imported in the declarated order. If the last object is a Hash or
     * Object we'll use it as the import options.
     *
     * For a list of options that you can pass see [[lily._importCSS]].
     * 
     * The module name follows Java's packaging conventions, where submodules
     * are separated by a dot. So if you require `my.game.module`, lily will
     * look for `my/game/module.css`.
     **/
    requireCSS: function()
    {
        var modules = $A(arguments);

        var arg = modules.pop();
        if (Object.isString(arg) || Object.isArray(arg) ||
            Object.isNumber(arg) || Object.isFunction(arg)) {
            modules.push(arg);
            arg = {};
        }

        for (var i=0, len=modules.length; i < len; ++i) {
            lily._importCSS(modules[i], arg);
        }
    },


    /**
     * lily.defineNamespace(namespace) -> undefined
     * - namespace (String): the namespace to define, using dot convention.
     * 
     * Defines the given namespace.
     * 
     * This will initialize all the parent namespace as an empty object (if
     * they haven't been initialized yet).
     **/
    defineNamespace: function(namespace)
    {
        var namespaces  = namespace.split(".");
        var parent      = window;

        while (namespaces.length > 0) {
            var object = namespaces.shift();
            if (object != "*") {
                if (!parent[object]) {
                    parent[object] = {};
                }
                parent = parent[object];
            }
        }
    },


    /**
     * lily.defineModule(module[, namespace_prefix][, namespace]) -> undefined
     * - module           (String): the module that was loaded.
     * - namespace        (String): an alias to use instead of the module's
     *                              name. 
     * - namespace_prefix (String): A prefix to use before the namespace.
     *                              defaults to `"lily."`
     * 
     * Registers the given module as successfully loaded.
     **/
    defineModule: function(module, namespace_prefix, namespace)
    {
        namespace_prefix || (namespace_prefix = "lily.");
        namespace = namespace_prefix + (namespace || module);

        this._imported.script[module] = "loaded";
        this.defineNamespace(namespace);
        if (this._import_callbacks[module]) {
            var modules = this._import_callbacks[module];
            var i = modules.length;
            while (i--) {
                // pops the callback function, then call it.
                modules.pop()(module);
            }
        }
    },


    /**
     * lily.onLoad([modules...], callback) -> undefined
     * - modules (String): a range of modules to wait for. If not defined
     *                      lily'll use the last imported ones.
     * - callback (Function): the function to call after the modules are
     *                        loaded. 
     * 
     * Registers a function to call when one or more modules are loaded.
     **/
    onLoad: function()
    {
        var modules = $A(arguments);
        var _callback = modules.pop();
        if (modules.length == 0) {
            modules = lily._last_imported;
        }

        function Callback()
        {
            this.wait_for = modules;
            this.callback = _callback;

            var cb = this;

            this.loaded = function(module) {
                cb.wait_for = cb.wait_for.without(module);
                if (cb.callback && cb.wait_for.length <= 0) {
                    cb.callback.defer();
                    cb.callback = null;
                }
            };
        }

        var l = modules.length;
        var cb = new Callback();
        while (l--) {
            var m = modules[l];
            if (this._imported.script[m] == "loaded") {
                cb.loaded(m);
            }
            else {
                if (!this._import_callbacks[m]) {
                    this._import_callbacks[m] = [];
                }
                this._import_callbacks[m].push(cb.loaded.bind(cb));
            }
        }
    },


    /**
     * lily.defineTemp(variable, value[, parent]) -> undefined
     * - variable (String): the name of the variable to define.
     * - value            : the value of the variable.
     * - parent   (Object): the namespace where the variable shall be defined.
     *                      defaults to `window`.
     * 
     * Defines a temporary variable, which can be reverted to its original
     * value later.
     * 
     * If `parent` is not given, the variable will be defined in the global
     * namespace (`window` object).
     **/
    defineTemp: function(variable, value, parent)
    {
        parent = parent || window;

        var old_value = parent[variable];
        parent[variable] = value;

        if (!lily._temp[parent]) {
            lily._temp[parent] = {};
        }
        lily._temp[parent][variable] = old_value;
    },


    /**
     * lily.revertTemp(variable, parent) -> undefined
     * - variable (String): the name of the variable
     * - parent   (Object): the namespace where the variable is defined.
     * 
     * Reverts a temporary variable to its original value.
     **/
    revertTemp: function(variable, parent)
    {
        if (lily._temp[parent]) {
            var old_value = lily._temp[parent][variable];
            parent[variable] = old_value;
            delete lily._temp[parent][variable];
        }
    },


    /**
     * lily.uniqueID() -> Number
     * 
     * Returns an unique object ID.
     **/
    uniqueID: function()
    {
        return ++lily.object_count;
    }
};

// initializes the lily package
lily._load();